package com.autoconfigure.redis.config;

import com.autoconfigure.redis.enumeration.RedisClient;
import com.autoconfigure.redis.handler.DefaultCacheErrorHandler;
import com.autoconfigure.redis.properties.RedisCacheProperties;
import com.autoconfigure.redis.properties.RedisConnectionProperties;
import com.google.common.base.Preconditions;
import com.google.gson.Gson;
import com.lemon.common.util.BeanDefinitionRegistryUtil;
import com.lemon.common.util.SpringContextHelper;
import com.lemon.constant.ConfigSwitchConstant;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizers;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.data.redis.CacheManagerFactory;
import org.springframework.boot.autoconfigure.data.redis.JedisCacheManagerFactory;
import org.springframework.boot.autoconfigure.data.redis.LettuceCacheManagerFactory;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.cache.CacheKeyPrefix;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.time.Duration;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author lemon
 * @description RedisCacheConfiguration 注解在 RedisAutoConfiguration 注解之后
 * {@link org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration} default 访问级别
 * 注解 @Bean 先生成 BeanDefinition，可理解为用 Spring 时候xml里面的<bean>标签。
 * {@link org.springframework.cache.interceptor.CacheAspectSupport} 缓存注解转操作
 * {@link org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration#cacheManagerCustomizers}
 * @date 2020-07-08 16:29
 */
@Slf4j
@Configuration
@EnableCaching
@AutoConfigureBefore(RedisAutoConfiguration.class)
@EnableConfigurationProperties(RedisCacheProperties.class)
@Import({JedisCacheManagerFactory.class, LettuceCacheManagerFactory.class})
@ConditionalOnProperty(name = ConfigSwitchConstant.REDIS_CACHE_CONFIG_ENABLE, havingValue = ConfigSwitchConstant.REDIS_CACHE_CONFIG_ENABLE_DEFAULT, matchIfMissing = true)
public class RedisCacheConfig extends CachingConfigurerSupport implements InitializingBean {
    @Value("${spring.app.cache.ttl:1800}")
    private int defaultTtl;

    @Value("${spring.app.cache.defaultCacheManagerName:cacheManager}")
    private String defaultCacheManagerName;

    @Value("${spring.app.cache.defaultRedisTemplateName:redisTemplate}")
    private String defaultRedisTemplateName;

    @Value("${spring.app.cache.defaultStringRedisTemplateName:stringRedisTemplate}")
    private String defaultStringRedisTemplateName;

    @Autowired
    private RedisCacheProperties redisProperties;

    @Autowired(required = false)
    @Qualifier("jedisCacheManagerFactory")
    private CacheManagerFactory jedisCacheManagerFactory;

    @Autowired(required = false)
    @Qualifier("lettuceCacheManagerFactory")
    private CacheManagerFactory lettuceCacheManagerFactory;

    private CacheManager defaultRedisCacheManager;

    private RedisTemplate<Object, Object> defaultRedisTemplate;

    private StringRedisTemplate defaultStringRedisTemplate;

    private RedisConnectionFactory defaultRedisConnectionFactory;

    @Autowired
    private CacheKeyPrefix cacheKeyPrefix;

    /**
     * @return void
     * @description
     * @author lemon
     * @date 2020-07-09 17:32
     */
    @Override
    public void afterPropertiesSet() {
        Preconditions.checkArgument(MapUtils.isNotEmpty(this.redisProperties.getConnections()), "Redis not configured");
        Preconditions.checkArgument((this.jedisCacheManagerFactory != null || this.lettuceCacheManagerFactory != null), "Must specify jedis or lettuce");
        String primary = this.redisProperties.getPrimary();

        if (!this.redisProperties.getConnections().containsKey(primary)) {
            primary = null;
            Set<Map.Entry<String, RedisConnectionProperties>> entrySet = this.redisProperties.getConnections().entrySet();

            for (Map.Entry<String, RedisConnectionProperties> entry : entrySet) {
                if (entry.getValue().getEnabled()) {
                    primary = entry.getKey();
                    break;
                }
            }

            Preconditions.checkArgument(StringUtils.isNotBlank(primary), "No Redis configuration enabled");

            this.redisProperties.setPrimary(primary);
            log.info("The default Redis data source is [{}]", primary);
        }

        this.redisProperties.getConnections().forEach((key, value) -> {
            if (StringUtils.isBlank(key)) {
                log.warn("Redis configuration id attribute is empty, config info:{}", new Gson().toJson(value));
                return;
            }

            // ttl
            if (value.getTtl() == null) {
                value.setTtl(this.redisProperties.getTtl());
            }

            if (value.getTtl() == null) {
                value.setTtl(Duration.ofSeconds(this.defaultTtl));
            }

            if (value.getUsePrefix() == null) {
                value.setUsePrefix(this.redisProperties.getUsePrefix());
            }

            if (value.getCacheNullValues() == null) {
                value.setCacheNullValues(this.redisProperties.getCacheNullValues());
            }

            if (value.getConversionService() == null) {
                value.setConversionService(this.redisProperties.getConversionService());
            }

            if (StringUtils.isBlank(value.getKeyPrefix())) {
                value.setKeyPrefix(this.redisProperties.getKeyPrefix());
            }

            // CacheKeyPrefix
            if (value.getCacheKeyPrefix() == null) {
                value.setCacheKeyPrefix(this.redisProperties.getCacheKeyPrefix());
            }
            if (value.getCacheKeyPrefix() == null) {
                value.setCacheKeyPrefix(this.cacheKeyPrefix);
            }

            value.getCacheNames().addAll(this.redisProperties.getCacheNames());
        });

        this.registerCacheManager();
    }

    /**
     * @param
     * @return void
     * @description
     * @author lemon
     * @date 2020-07-10 14:24
     */
    public void registerCacheManager() {
        RedisConnectionProperties defaultRedisConnection = this.redisProperties.getConnections().remove(this.redisProperties.getPrimary());
        CacheManagerFactory defaultCacheManagerFactory = this.getCacheManagerFactory(defaultRedisConnection);
        log.debug("Use [{}] to create RedisCacheManager", defaultCacheManagerFactory.getClass());


        if (defaultRedisConnection.getLettuce() != null) {
            this.doRegisterCacheManager(defaultCacheManagerFactory, defaultRedisConnection, this.redisProperties.getPrimary(), true);
        }

        this.redisProperties.getConnections().forEach((key, redisConnectionProperties) -> {
            if (!redisConnectionProperties.getEnabled()) {
                log.debug("Redis config [{}] is not enabled", key);
                return;
            }

            CacheManagerFactory cacheManagerFactory = this.getCacheManagerFactory(defaultRedisConnection);
            log.debug("Use [{}] to create RedisCacheManager", cacheManagerFactory.getClass());
            this.doRegisterCacheManager(cacheManagerFactory, redisConnectionProperties, key, false);
        });
    }

    /**
     * @param redisConnectionProperties
     * @return org.springframework.boot.autoconfigure.data.redis.CacheManagerFactory
     * @description
     * @author lemon
     * @date 2020-07-09 18:15
     */
    public CacheManagerFactory getCacheManagerFactory(RedisConnectionProperties redisConnectionProperties) {
        if (this.lettuceCacheManagerFactory == null) {
            return this.jedisCacheManagerFactory;
        }

        if (this.jedisCacheManagerFactory == null) {
            return this.lettuceCacheManagerFactory;
        }

        if (RedisClient.JEDIS.equals(redisConnectionProperties.getClient())) {
            return this.jedisCacheManagerFactory;
        }

        return this.lettuceCacheManagerFactory;
    }

    /**
     * @param cacheManagerFactory
     * @param redisConnectionProperties
     * @param cacheId
     * @param defaultConfig
     * @return void
     * @description
     * @author lemon
     * @date 2020-07-09 17:49
     */
    public void doRegisterCacheManager(CacheManagerFactory cacheManagerFactory, RedisConnectionProperties redisConnectionProperties, String cacheId, boolean defaultConfig) {
        log.info("Redis config [{}] is {}", cacheId, new Gson().toJson(redisConnectionProperties));

        Preconditions.checkNotNull(cacheManagerFactory, "No CacheManagerFactory");

        RedisConnectionFactory redisConnectionFactory;

        try {
            redisConnectionFactory = cacheManagerFactory.createConnectionFactory(redisConnectionProperties);
        } catch (Exception e) {
            String tip = String.format("Failed to create connection with redis configuration with ID attribute [%s]", cacheId);
            throw new RuntimeException(tip, e);
        }

        CacheManager cacheManager = cacheManagerFactory.createCacheManager(redisConnectionFactory, redisConnectionProperties);

        if (defaultConfig) {
            this.defaultRedisCacheManager = cacheManager;
            this.defaultRedisTemplate = this.redisTemplate(redisConnectionFactory);
            this.defaultStringRedisTemplate = this.stringRedisTemplate(redisConnectionFactory);
            this.defaultRedisConnectionFactory = redisConnectionFactory;

            BeanDefinitionRegistryUtil.registerSingleton(this.defaultCacheManagerName, cacheManager);
            BeanDefinitionRegistryUtil.registerSingleton(this.defaultRedisTemplateName, this.redisTemplate(redisConnectionFactory));
            BeanDefinitionRegistryUtil.registerSingleton(this.defaultStringRedisTemplateName, this.stringRedisTemplate(redisConnectionFactory));
            return;
        }

        String cacheManagerName = defaultConfig ? this.defaultCacheManagerName : this.defaultCacheManagerName + cacheId;
        String redisTemplateName = defaultConfig ? this.defaultRedisTemplateName : this.defaultRedisTemplateName + cacheId;
        String stringRedisTemplateName = defaultConfig ? this.defaultStringRedisTemplateName : this.defaultStringRedisTemplateName + cacheId;

        this.doRegisterCacheManager(cacheManagerName, RedisCacheManager.class, cacheManager);
        this.doRegisterCacheManager(redisTemplateName, RedisTemplate.class, this.redisTemplate(redisConnectionFactory));
        this.doRegisterCacheManager(stringRedisTemplateName, StringRedisTemplate.class, this.stringRedisTemplate(redisConnectionFactory));
    }

    /**
     * @param beanName
     * @param clazz
     * @param beanObject
     * @return void
     * @description
     * @author lemon
     * @date 2020-07-11 09:19
     */
    public <T> void doRegisterCacheManager(String beanName, Class<T> clazz, Object beanObject) {
        BeanDefinitionRegistryUtil.registerBeanDefinitionNoPrimary(beanName, clazz);
        BeanDefinitionRegistryUtil.registerSingleton(beanName, beanObject);

        log.debug("Bean [{}] is {}", beanName, SpringContextHelper.getBean(beanName));
        log.info("BeanDefinition [{}] is {}", beanName, BeanDefinitionRegistryUtil.getBeanDefinition(beanName));
    }

    @Bean
    public CacheManagerCustomizers cacheManagerCustomizers(ObjectProvider<CacheManagerCustomizer<?>> customizers) {
        return new CacheManagerCustomizers(customizers.orderedStream().collect(Collectors.toList()));
    }

    /**
     * @param
     * @return org.springframework.data.redis.cache.CacheKeyPrefix
     * @description
     * @author lemon
     * @date 2020-07-10 14:58
     */
    @Bean
    @ConditionalOnMissingBean
    public CacheKeyPrefix cacheKeyPrefix() {
        return new DefaultCacheKeyPrefix();
    }

    /**
     * @param
     * @return org.springframework.data.redis.connection.RedisConnectionFactory
     * @description 默认  RedisConnectionFactory
     * @author lemon
     * @date 2020-07-10 12:32
     */
    @Bean
    @ConditionalOnMissingBean
    public RedisConnectionFactory redisConnectionFactory() {
        return this.defaultRedisConnectionFactory;
    }

    /**
     * @param
     * @return org.springframework.data.redis.core.RedisTemplate<java.lang.Object, java.lang.Object>
     * @description 默认 RedisTemplate
     * {@link org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration#redisTemplate} @ConditionalOnMissingBean(name = "redisTemplate") 可以检测到 redisTemplate Bean
     * @author lemon
     * @date 2020-07-10 11:07
     */
    @Bean
    @ConditionalOnMissingBean
    public RedisTemplate<Object, Object> redisTemplate() {
        return defaultRedisTemplate;
    }

    /**
     * @param
     * @return org.springframework.data.redis.core.StringRedisTemplate
     * @description 默认 StringRedisTemplate
     * @author lemon
     * @date 2020-07-10 11:07
     */
    @Bean
    @ConditionalOnMissingBean
    public StringRedisTemplate stringRedisTemplate() {
        return defaultStringRedisTemplate;
    }

    /**
     * @param
     * @return org.springframework.cache.CacheManager
     * @description 默认缓存管理器
     * @author lemon
     * @date 2020-07-10 11:06
     */
    @Bean
    @Override
    @ConditionalOnMissingBean
    public CacheManager cacheManager() {
        return this.defaultRedisCacheManager;
    }

    /**
     * @param
     * @return org.springframework.cache.interceptor.CacheErrorHandler
     * @description 缓存异常处理
     * @author lemon
     * @date 2020-07-08 12:42
     */
    @Override
    public CacheErrorHandler errorHandler() {
        log.info("基于注解缓存使用，Redis缓存故障操作缓存时异常处理策略");

        return new DefaultCacheErrorHandler();
    }

    /**
     * @param redisConnectionFactory
     * @return org.springframework.data.redis.core.RedisTemplate<java.lang.Object, java.lang.Object>
     * @description
     * @author lemon
     * @date 2020-07-09 17:25
     */
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    /**
     * @param redisConnectionFactory
     * @return org.springframework.data.redis.core.StringRedisTemplate
     * @description
     * @author lemon
     * @date 2020-07-09 17:25
     */
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}